package fsmsim.simulator;


import fsmsim.buffer.Buffer;
import fsmsim.dataStructure.Container;
import fsmsim.dataStructure.Event;
import fsmsim.dataStructure.FSM;
import fsmsim.dataStructure.Transition;
import fsmsim.exception.FSMException;
import fsmsim.graphicUI.FSMSimInterface;
import fsmsim.parserXML.CoherenceCheck;
import fsmsim.parserXML.ParserXML;
import fsmsim.parserXML.ValidateXML;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * <p>Title: FSMSim</p>
 *
 * <p>Description: Simulatore di macchine a stati finiti.</p>
 *
 * <p>Copyright: Copyright (c) 2008</p>
 *
 * <p>Company: Bollati, Donati, Gabrielli, Peli</p>
 *
 * @author Bollati, Donati, Gabrielli, Peli
 * @version 3.0
 */


public class Controller implements Runnable{
    
    public static String fileXMLSchema = "fileXMLSchema.xsd"; //modifiche future: fare come file xml
    
    private Container externalEventsList;
    private Vector<FSM> fsms;
    private Buffer buffer;
    
    private String fileUrl;
    
    private FSMSimInterface ui;
    
    private Thread th;
    
    
    /**
     * Contruttore
     * @param ui
     */
    
    public Controller(FSMSimInterface ui) {
        this.externalEventsList = null;
        
        this.fsms = null;
        
        this.buffer = null;
        
        this.ui = ui;
        
        this.th = new Thread (this, "Thread Sim");
        
       
        // add listener all'interfaccia grafica
        ui.addNonDeterminismListenerButton(new SolveNonDeterminismListener());
        ui.addNewChooserFileListenerButton(new NewChooserFileListener());
        ui.addSimulationListenerButton(new SimulationListener());
        ui.addLoadSimulationButton(new LoadSimulationListener());
        ui.addStepToStepButton(new StepToStepListener());
        ui.addRapidButton(new RapidListener());
        ui.addStepToStepComboBox(new StepToStepListener());
        ui.addRapidComboBox(new RapidListener());
        ui.addStepToStepMenuItem(new StepToStepListener());
        ui.addRapidMenuItem(new RapidListener());
        ui.addAbout(new AboutListener());
    }
    
    
    /**
     * Restituisce l'oggetto buffer
     * @return
     */
    
    public Buffer getBuffer() {
        return buffer;
    }

    
    /**
     * Associa all'oggetto buffer locale quello passato per parametro  
     * @param buffer
     */
    
    public void setBuffer(Buffer buffer) {
        this.buffer = buffer;
    }

    
    /**
     * Restituisce il vettore di macchine a stati finiti
     * @return
     */
    
    public Vector getFsms() {
        return fsms;
    }
    
    
    /**
     * Associa all'oggetto locale quello passato per parametro  
     * @param fsms
     */
    
    public void setFsms(Vector fsms) {
        this.fsms = fsms;
    }
    
    
    /**
     * Restituisce un oggetto di tipo FSM, dato l'indice i passato come parametro
     * @param i
     * @return
     */
    
    public FSM getFsm(int i) {
        return this.fsms.get(i);
    }
    
    
    /**
     * Inserisce ad un determinato indice la macchina a stati finiti passata per parametro
     * @param index
     * @param fsm
     */
    
    public void setFsm(int index, FSM fsm) {
        this.fsms.add(index, fsm);
    }
    
    
    /**
     * Aggiunge una macchina a stati finiti al vettore delle macchine a stati finiti
     * @param fsm
     */
    
    public void addFsm(FSM fsm) {
        this.fsms.add(fsm);
    }

    
    /**
     * Restituisce un oggetto di tipo FSMSimInterface
     * @return
     */
    
    public FSMSimInterface getUi() {
        return ui;
    }

    
    /**
     * Associa ad un oggetto di tipo FSMSimInterface quello passato per parametro
     * @param ui
     */
    
    public void setUi(FSMSimInterface ui) {
        this.ui = ui;
    }
    
    
    /**
     * Restituisce l'oggetto di tipo Controller
     * @return
     */
    
    public Controller getController() {
        return this;
    }

    
    /**
     * Restituisce la lista degli eventi esterni
     * @return
     */
    
    public Container getOutputEventList() {
        return externalEventsList;
    }
    
    
    /**
     * Associa al contenitore di eventi esterni quello passato per parametro
     * @param externalEventsList
     */
    
    public void setOutputEventList(Container externalEventsList) {
        this.externalEventsList = externalEventsList;
    }

    
    /**
     * Listener per il non determinismo
     */
    
    class SolveNonDeterminismListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {

            ui.closeChoices();
                
        }
    }
     
    
    /**
     * Listener per la scelta di una nuova simulazione
     */
    
    class NewChooserFileListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
                
            fileUrl = ui.getFileUrl();
                              
        }
    }
    
    
    /**
     * Listener per il caricamento dei dati nelle strutture dati
     */
    
    class LoadSimulationListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            
            ui.setModality();
            ui.setReinit(true);
            ui.cleanOutput();
            
            try {
                
                validateXML();
                parsingXML();
                checkCoherenceXML();
                
            }
            catch (FSMException ex) {
                ui.setReinit(false);
                ui.messageBox("Error!", ex.getErrorMessage());
                ui.statusBarMessage(
                    "Un errore non ha permesso di caricare la simulazione. " +
                    "Crearne una nuova");
                Logger.getLogger(Controller.class.getName()).log(Level.SEVERE,
                        null, ex);
            }
            ui.update(externalEventsList, fsms, buffer);
            ui.closeDlgNuova();
        }
    }
    
    
    /**
     * Listener per la simulazione Step to Step
     */
    
    class StepToStepListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            
            ui.setStep2StepSimulation();
            
        }
    }
    
    
    /**
     * Listener per la simulazione Rapida
     */
    
    class RapidListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
        
            ui.setRapidSimulation();
            
        }
    }
     
    
    /**
     * Listener per l'about personalizzato
     */
                   
    class AboutListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            
            ui.setTextAbout("Bollati Oscar, Donati Antonio, Gabrielli Paolo," +
                    " Peli Stefano and Cristini Fabio (for the 1.0)",
                        "Versione 3.0", "Simulatore di macchine a stati finiti.");
        }
    }
       
    
    /**
     * Listener per l'avvio della simulazione
     */
    
    class SimulationListener implements ActionListener {
        public void actionPerformed(ActionEvent e) {
            
            th = new java.lang.Thread (getController(), "Thread Sim"); 
            th.start();
          
        }
    }

    
    /**
     * Effettua il parsing richiamando metodi della classe ParserXML
     * @throws fsmsim.exception.FSMException
     */
        
    private void parsingXML() throws FSMException {
        ParserXML parserXml = new ParserXML(fileUrl);
        parserXml.doParsingXML(this);
    }
    
    
    /**
     * Valida il file XML
     * @throws fsmsim.exception.FSMException
     */
    
    private void validateXML() throws FSMException {
    // validazione del file xml, da spostare nel metodo per acquisire dati
        ValidateXML validateXml = new ValidateXML(fileXMLSchema);
        if (!validateXml.validateXML(fileUrl.toString())) {
            throw new FSMException("Il File Xml:\t" +this.fileUrl + "\t non è valido");           
        }
    }
    
    
    /**
     * Effettua il controllo di coerenza sui dati immessi nel sistema
     * @throws fsmsim.exception.FSMException
     */
    
    private void checkCoherenceXML() throws FSMException {
        CoherenceCheck coherenceCheck = new CoherenceCheck();
        coherenceCheck.checkCoherence(this.getFsms(), this.getOutputEventList(), this.getBuffer());
    }      
      
    
    /*
     * Effettua la scelta tra le FSM
     * 
     */
    
    private int getNonDeterminismMachineChoices(Vector<FSM> fsms) {
        String[] machines = new String[fsms.size()];
        for (int i = 0 ; i < this.fsms.size() ; i++) {
            if (fsms.contains(this.fsms.get(i)))
                machines[i] = "FSM " + (i + 1);
        }
        return selectChoices(machines);
    }
    
    
    /**
     * Effettua la scelta tra le transizioni
     * @param t
     * @return
     */
    
    private int getNonDeterminismTransitionChoices(Container t) {
        String[] s = new String[t.size()];
        for (int i = 0; i < t.size(); i++)
            s[i] = t.getElement(i).toString();
        return selectChoices(s);
    }
    
    
    /**
     * Seleziona la scelta dell'utente e la presenta come parametro di ritorno
     * @param selection
     * @return
     */
    
    private int selectChoices(String[] selection) {
        ui.select(selection);
        return ui.getChoices();
    }
    
    
    /**
     * Visualizza lo stato finale del sistema
     */
    
    private void lastView() {
        ui.consoleOut("\nFinito!!");
        ui.displaySystemState(fsms, buffer.toString(), externalEventsList.toString());
        ui.messageBox("Informazione", "Simulazione terminata!");
        try {
            this.th.join();
        } catch (InterruptedException ex) {
            Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    
    /**
     * Esegue la transizione in una determinata FSM con l'evento in input passato come parametro
     * @param fsm
     * @param actualEvent
     * @return
     */
    
    private Event executeTransition(FSM fsm, Event actualEvent) {
        fsm.setFinished(false);
        Container t = fsm.searchEnabledTransitions(actualEvent);

        if (t != null) {
            if (t.size() == 1) {
                return fsm.executeTransition((Transition) t.getElement(0));
            }
            else {
                int i = getNonDeterminismTransitionChoices(t);
                return fsm.executeTransition((Transition) t.getElement(i));
            }
        } 
        else {
            fsm.setFinished(true);
            return null;
        }
    }
    
    
    /**
     * Visualizza gli elementi del sistema in un dato istante della simulazione
     */

    private void display() {
        try {
            ui.update(externalEventsList, fsms, buffer);           
            Thread.sleep(ui.getDelay());
        } 
        catch (InterruptedException ex) {
            Logger.getLogger(Controller.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    
    
    /**
     * Esegue la transizione data la scelta effettuata e dato l'evento in ingresso
     * @param choice
     * @param acutalEvent
     */
    
    private void chooseAndGet(int choice, Event acutalEvent) {
        executeTransition(isEnabledReads(acutalEvent).get(choice), acutalEvent);
        // rimuovilo dal buffer
        buffer.drawEvent();
    }
    
    
    /**
     * Consuma, se possibile, gli eventi interni nel buffer
     */

    private void useInternalEvents() {
        Event actualEvent;
        while ((actualEvent = buffer.getEvent()) != null) {
                if (isEnabledReads(actualEvent).size() == 0)
                    break;
                display();
                if (isEnabledReads(actualEvent).size() > 1) {
                    // non determinismo tra macchine e nella stessa macchina
                    int choice = getNonDeterminismMachineChoices(isEnabledReads(actualEvent));
                    chooseAndGet(choice, actualEvent);
                    
                }
                else {
                    if (isEnabledReads(actualEvent).size() == 1) {
                        chooseAndGet(0, actualEvent);
                    }
                }
        }
    }
    
    
    /**
     * Ritorna le fsm abilitate dall'evento passato
     * @param event
     * @return
     */
    
    private Vector<FSM> isEnabledAll(Event aEvent) {
        Vector<FSM> temp = new Vector<FSM>();
        for (int i = 0 ; i < fsms.size() ; i++)
            if (fsms.get(i).isEnabled(aEvent))
                temp.addElement(fsms.get(i));
        return temp;
    }
    
    
    /**
     * Ritorna le fsm abilitate dall'evento passato e che siano delle FSM lettrici
     * @param aEvent
     * @return
     */
    
    private Vector<FSM> isEnabledReads(Event aEvent) {
        Vector<FSM> temp = new Vector<FSM>();
        for (int i = 0 ; i < fsms.size() ; i++)
            if ((fsms.get(i).isEnabled(aEvent)) && (fsms.get(i).isReader()))
                temp.addElement(fsms.get(i));
        return temp;
    }
    
    
    /**
     * Seleziona l'opportuna FSM, data la scelta dell'utente, e esegue la transizione inserendo nel buffer
     * l'evento di uscita di tale transizione
     * @param choice
     * @param ActualEvent
     */
    
    private void chooseAndAdd(int choice, Event ActualEvent) {
        FSM selectedFSM = isEnabledAll(ActualEvent).get(choice);
        Event ei = executeTransition(isEnabledAll(ActualEvent).get(choice), ActualEvent);
        if (!selectedFSM.isReader()) {
             // la macchina può consumare questo evento   
                if (ei != null)
                    // ok, ha prodotto un evento
                    buffer.addEvent(ei);
                    // la macchina è scattata, metto in moto le READ sugli eventi interni
        }
    }
    
    
    /**
     * è il metodo che gestisce l'evoluzione della simulazione
     */
    
    public void run() {
    
        Event attuale;

        boolean finito = false;
        
        useInternalEvents();
        
        do {
            display();           
            
            attuale = (Event) this.externalEventsList.drawElement();                   
            
            if (isEnabledAll(attuale).size() == 0) {
                externalEventsList.addElement(attuale, 0);
                lastView();
                finito = true;    
            }
            else {
                if (isEnabledAll(attuale).size() > 1) {
                    // non determinismo tra macchine e nella stessa macchina
                    int choice = getNonDeterminismMachineChoices(isEnabledAll(attuale));
                    chooseAndAdd(choice,attuale);
                }
                else {
                    if (isEnabledAll(attuale).size() == 1) {
                        // determinismo
                        chooseAndAdd(0,attuale);
                    }
                }
            }
            
            useInternalEvents();
            
        }while ((!finito) && (attuale != null));    
    }
    
}
